﻿/*
 * Copyright (c) Martin Kinkelin
 *
 * See the "License.txt" file in the root directory for infos
 * about permitted and prohibited uses of this code.
 */

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Windows.Forms;

namespace Jarf
{
	/// <summary>
	/// Encapsulates a Rsync child process.
	/// Manages reading the process' output, disposing of the process
	/// and invoking event handlers.
	/// </summary>
	public class RsyncProcess : IDisposable
	{
		private readonly object _syncObject = new object();

		private bool _isRunning;
		private int _exitCode = -1;

		private List<string> _output = new List<string>();
		private string _fullOutput;



		/// <summary>
		/// Gets the associated backup task.
		/// </summary>
		public BackupTask Task { get; private set; }

		/// <summary>
		/// Gets the actual process.
		/// </summary>
		public Process Process { get; private set; }

		/// <summary>
		/// Gets a value indicating whether the process is currently running.
		/// </summary>
		public bool IsRunning
		{
			get
			{
				lock (_syncObject)
				{
					return _isRunning;
				}
			}
		}

		/// <summary>
		/// Gets the exit code of the process.
		/// </summary>
		public int ExitCode
		{
			get
			{
				lock (_syncObject)
				{
					if (_exitCode >= 0)
						return _exitCode;
				}

				throw new InvalidOperationException("The process has not exited yet.");
			}
		}

		/// <summary>
		/// Gets the lines written to stdout and stderr.
		/// </summary>
		public List<string> Output
		{
			get
			{
				lock (_syncObject)
				{
					if (_exitCode >= 0)
						return _output;
				}

				throw new InvalidOperationException("The process has not exited yet.");
			}
		}

		/// <summary>
		/// Gets the full text written to stdout and stderr.
		/// </summary>
		public string FullOutput
		{
			get
			{
				if (_fullOutput != null)
					return _fullOutput;

				StringBuilder output = new StringBuilder(32768);

				// check if the process has exited
				List<string> lines = Output;

				foreach (string line in lines)
				{
					output.AppendLine(line);
				}

				_fullOutput = output.ToString();

				return _fullOutput;
			}
		}

		/// <summary>
		/// Gets the number of transferred files.
		/// </summary>
		public int NumTransferredFiles
		{
			get
			{
				// check if the process has exited
				for (int i = Output.Count - 1; i >= 0; --i)
				{
					if (_output[i].StartsWith("Number of files transferred: "))
						return int.Parse(_output[i].Substring(29));
				}

				throw new InvalidOperationException("The number of transferred files could not be parsed. " +
					"Is the \"--stats\" command line option missing?");
			}
		}

		/// <summary>
		/// Gets the number of deleted files and folders.
		/// </summary>
		public int NumDeletions
		{
			get
			{
				int num = 0;

				// check if the process has exited
				List<string> lines = Output;

				foreach (string line in lines)
				{
					if (line.StartsWith("del. "))
						++num;
				}

				return num;
			}
		}



		/// <summary>
		/// Fired when the process has been started.
		/// </summary>
		public event EventHandler Started;

		/// <summary>
		/// Fired when the process has written a line to stdout or stderr.
		/// </summary>
		public event EventHandler<DataReceivedEventArgs> LineWritten;

		/// <summary>
		/// Fired when the process has exited.
		/// Not fired if the process is killed.
		/// </summary>
		public event EventHandler Exited;



		/// <summary>
		/// Constructor.
		/// </summary>
		/// <param name="task">Associated BackupTask.</param>
		/// <param name="process">Process to be encapsulated.</param>
		public RsyncProcess(BackupTask task, Process process)
		{
			if (task == null)
				throw new ArgumentNullException("task");
			if (process == null)
				throw new ArgumentNullException("process");

			Task = task;
			Process = process;

			Process.StartInfo.RedirectStandardOutput = true;
			Process.StartInfo.StandardOutputEncoding = Encoding.UTF8;
			Process.OutputDataReceived += new DataReceivedEventHandler(Process_DataReceived);

			Process.StartInfo.RedirectStandardError = true;
			Process.StartInfo.StandardErrorEncoding = Encoding.UTF8;
			Process.ErrorDataReceived += new DataReceivedEventHandler(Process_DataReceived);

			Process.EnableRaisingEvents = true;
			Process.Exited += new EventHandler(Process_Exited);
		}

		/// <summary>
		/// Destructor.
		/// </summary>
		~RsyncProcess()
		{
			Dispose();
		}


		/// <summary>
		/// Disposes of the process after killing it if it is currently running.
		/// </summary>
		public void Dispose()
		{
			if (Process != null)
			{
				Kill();
				Process.Close();
				Process = null;
			}
		}

		/// <summary>
		/// Starts the process.
		/// </summary>
		public void Start()
		{
			lock (_syncObject)
			{
				if (_isRunning)
					throw new InvalidOperationException("The process is already running.");

				_exitCode = -1;
				_output.Clear();
				_fullOutput = null;

				Process.EnableRaisingEvents = true;

				Process.Start();
				_isRunning = true;

				Process.BeginOutputReadLine();
				Process.BeginErrorReadLine();
			}

			if (Started != null)
				FireEvent(Started, EventArgs.Empty);
		}

		/// <summary>
		/// Kills the process if it is currently running without firing the Exited event.
		/// </summary>
		public void Kill()
		{
			lock (_syncObject)
			{
				if (_isRunning)
				{
					Process.EnableRaisingEvents = false;

					// the process might just have exited, therefore use a try-catch block
					try
					{
						Process.Kill();
						_exitCode = 0;
					}
					catch
					{
						_exitCode = Process.ExitCode;
					}

					_isRunning = false;
				}
			}
		}


		/// <summary>
		/// Invoked asynchronously when the process has written a line to stdout or stderr.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void Process_DataReceived(object sender, DataReceivedEventArgs e)
		{
			if (e.Data == null)
				return;

			lock (_syncObject)
			{
				_output.Add(e.Data);
			}

			if (LineWritten != null)
				FireEvent(LineWritten, e);
		}

		/// <summary>
		/// Invoked asynchronously when the process has exited.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void Process_Exited(object sender, EventArgs e)
		{
			lock (_syncObject)
			{
				_isRunning = false;
				_exitCode = Process.ExitCode;
			}

			if (Exited != null)
				FireEvent(Exited, EventArgs.Empty);
		}


		/// <summary>
		/// Fires the specified event.
		/// All event handlers are scanned for methods of Control instances.
		/// If the current thread is not the one which created the control,
		/// the control's method is invoked in the control's thread.
		/// </summary>
		/// <param name="ev">Event to be fired.</param>
		/// <param name="e">EventArgs instance for the event handlers.</param>
		private void FireEvent(MulticastDelegate ev, EventArgs e)
		{
			Delegate[] delegates = ev.GetInvocationList();

			object[] args = new object[] { this, e };

			foreach (Delegate d in delegates)
			{
				Control control = d.Target as Control;

				if (control != null && control.InvokeRequired)
				{
					// invoke the Control method in its thread
					control.Invoke(d, args);
				}
				else
				{
					// invoke the delegate normally
					d.Method.Invoke(d.Target, args);
				}
			}
		}
	}
}
